/**
* Copyright (c) By zengqh.
*
* This program is just for fun or demo, in the hope that it  
* will be useful, you can redistribute it and/or modify freely.
*
* Time: 2013/05/21
* File: Camera.cpp
* Blog: http://www.cnblogs.com/zengqh/
**/

#include "enn_camera.h"

namespace enn
{
Camera::Camera()
{
	dirty_flags_.set_flag(DT_VIEW_MATRIX);
	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

Camera::~Camera()
{

}

void Camera::setNearPlane(float near_clip)
{
	near_clip_ = Max(near_clip, 0.01f);

	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

void Camera::setFarPlane(float far_clip)
{
	far_clip = Max(far_clip, 0.01f);

	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

void Camera::setFov(float fov)
{
	fov = fov;

	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

void Camera::setOrthoSize(float othro)
{
	ortho_size_ = othro;
	aspect_ratio_ = 1.0f;

	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

void Camera::setOrthoSize(const vec2f& size)
{
	ortho_size_ = size.y_;
	aspect_ratio_ = size.x_ / size.y_;

	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

void Camera::setAspectRatio(float ratio)
{
	aspect_ratio_ = ratio;

	dirty_flags_.set_flag(DT_PROJ_MATRIX);
	dirty_flags_.set_flag(DT_FRUSTUM);
}

float Camera::getNearClip() const
{
	if (isPerspective())
	{
		return near_clip_;
	}
	else
	{
		return 0.0f;
	}
}

float Camera::getFarClip() const
{
	if (isPerspective())
	{
		return far_clip_;
	}
	else
	{
		return 0.0f;
	}
}

float Camera::getFov() const
{
	return fov_;
}

float Camera::getOrthoSize() const
{
	return ortho_size_;
}

float Camera::getAspectRatio() const
{
	return aspect_ratio_;
}

const Frustum& Camera::getFrustum() const
{
	if (dirty_flags_.test_flag(DT_FRUSTUM))
	{
		if (isPerspective())
		{
			frustum_.Define(fov_, aspect_ratio_, 1.0f, getNearClip(), far_clip_, world_matrix_);
		}	
		else
		{
			frustum_.DefineOrtho(ortho_size_, aspect_ratio_, 1.0f, getNearClip(), far_clip_, world_matrix_);
		}

		dirty_flags_.reset_flag(DT_FRUSTUM);
	}

	return frustum_;
}

const Matrix4& Camera::getProjMatrix() const
{
	if (dirty_flags_.test_flag(DT_PROJ_MATRIX))
	{
		makePerspectiveProj();
		dirty_flags_.reset_flag(DT_PROJ_MATRIX);
	}

	return proj_matrix_;
}

const Matrix4& Camera::getViewMatrix() const
{
	return view_matrix_;
}

Frustum Camera::getSplitFrustum(float near_clip, float far_clip)
{
	Frustum ret;
	if (isPerspective())
	{
		ret.Define(fov_, aspect_ratio_, 1.0f, near_clip, far_clip, world_matrix_);
	}	
	else
	{
		ret.DefineOrtho(ortho_size_, aspect_ratio_, 1.0f, near_clip, far_clip, world_matrix_);
	}

	return ret;
}

Frustum Camera::getViewspaceFrustum() const
{
	Frustum ret;

	if (isPerspective())
	{
		ret.Define(fov_, aspect_ratio_, 1.0f, near_clip_, far_clip_);
	}	
	else
	{
		ret.DefineOrtho(ortho_size_, aspect_ratio_, 1.0f, near_clip_, far_clip_);
	}

	return ret;
}

Frustum Camera::getViewSpaceSplitFrustum(float near_clip, float far_clip) const
{
	Frustum ret;
	if (isPerspective())
	{
		ret.Define(fov_, aspect_ratio_, 1.0f, near_clip, far_clip);
	}	
	else
	{
		ret.DefineOrtho(ortho_size_, aspect_ratio_, 1.0f, near_clip, far_clip);
	}

	return ret;
}

vec2f Camera::worldToScreenPoint(const vec3f& worldPos)
{
	vec2f ret;

	vec3f eye_space_pos = getViewMatrix() * worldPos;
	
	if(eye_space_pos.z_ < 0.0f)
	{
		vec3f screenSpacePos = getProjMatrix() * eye_space_pos;
		ret.x_ = screenSpacePos.x_;
		ret.y_ = screenSpacePos.y_;
	}
	else
	{
		ret.x_ = (-eye_space_pos.x_ > 0.0f) ? -1.0f : 1.0f;
		ret.y_ = (-eye_space_pos.y_ > 0.0f) ? -1.0f : 1.0f;
	}

	ret.x_ = (ret.x_ / 2.0f) + 0.5f;
	ret.y_ = 1.0f - ((ret.y_ / 2.0f) + 0.5f);
	return ret;
}

vec3f Camera::screenToWorldPoint(const vec3f& screenPos)
{
	Ray ray = GetScreenRay(screenPos.x_, screenPos.y_);
	return ray.origin_ + ray.direction_ * screenPos.z_;
}

vec3f Camera::getForwardVector()
{
	return world_matrix_.RotationMatrix() * vec3f::FORWARD;
}

vec3f Camera::getRightVector()
{
	return world_matrix_.RotationMatrix() * vec3f::RIGHT;
}

vec3f Camera::getUpVector()
{
	return world_matrix_.RotationMatrix() * vec3f::UP;
}

float Camera::getDistance(const vec3f& worldPos) const
{
	if (isPerspective())
	{
		const vec3f& camera_pos = world_matrix_.Translation();

		return (worldPos - camera_pos).Length();
	}

	return 0.0f;
}

float Camera::getDistanceSquared(const vec3f& worldPos) const
{
	if (isPerspective())
	{
		const vec3f& camera_pos = world_matrix_.Translation();

		return (worldPos - camera_pos).LengthSquared();
	}

	return 0.0f;
}

void Camera::setTransform(const vec3f& eye, const vec3f& target, const vec3f& up)
{
	world_matrix_.makeTransform(eye, target - eye, up);
	view_matrix_ = world_matrix_.Inverse();
}

void Camera::setTransform(const Matrix4& matWorld)
{
	world_matrix_ = matWorld;
	view_matrix_ = world_matrix_.Inverse();
}

void Camera::setProjType(uint32 type)
{
	proj_type_ = type;
}

uint32 Camera::getProjType() const
{
	return proj_type_;
}

bool Camera::isPerspective() const
{
	return proj_type_ == PT_PERSPECTIVE;
}

bool Camera::isOrthographic() const
{
	return proj_type_ == PT_ORTHOGRAPHIC;
}

void Camera::makePerspectiveProj() const
{
	float nearClip = getNearClip();
	float h = (1.0f / tanf(fov_ * static_cast<float>(ENN_DEGTORAD) * 0.5f));
	float w = h / aspect_ratio_;
	float q, r;

	q = (far_clip_ + nearClip) / (far_clip_ - nearClip);
	r = -2.0f * far_clip_ * nearClip / (far_clip_ - nearClip);

	proj_matrix_.m00_ = w;
	proj_matrix_.m02_ = 0;
	proj_matrix_.m11_ = h;
	proj_matrix_.m12_ = 0;
	proj_matrix_.m22_ = q;
	proj_matrix_.m23_ = r;
	proj_matrix_.m32_ = 1.0f;
}

void Camera::makeOrthoProj() const
{
	// Disregard near clip, because it does not affect depth precision as with perspective projection
	float h = (1.0f / (ortho_size_ * 0.5f));
	float w = h / aspect_ratio_;
	float q, r;

	q = 2.0f / far_clip_;
	r = -1.0f;

	proj_matrix_.m00_ = w;
	proj_matrix_.m03_ = 0;
	proj_matrix_.m11_ = h;
	proj_matrix_.m13_ = 0;
	proj_matrix_.m22_ = q;
	proj_matrix_.m23_ = r;
	proj_matrix_.m33_ = 1.0f;
}

}